Skip to content

Conversation

@amnguye
Copy link
Member

@amnguye amnguye commented Oct 23, 2025

Attempt to address: #49993

In order to ensure polling and scanning operations use the "target" BlobServiceClient specified we had to make changes to how Listeners are added and the BlobListenerStrategy is initialized.

In my attempt here, I did the following:

  • Changed IBlobNotificationStrategy to now take in a "target" BlobServiceClient
    • This updated all the derived *Strategy's as well
  • Updated the PollLogsStrategy check on if "$logs" is enabled on the Blob Storage Account
    • I added a try-catch here because ScanBlobScanLogHybridPollingStrategy will eventually call this down the line to prevent unncessary double calls to GetProperties and SetProperties.
  • Fall back to the primary blob storage account
    • This was done to maintain behavior in the case that users want the primary account to be used

TODO:

  • Write up unit tests
  • Ensure the error code being caught for permissions check is correct, to fall back to primary account

…y.RegisterAsync; Added catch for possible permissions failure to default to primary
@github-actions github-actions bot added the Storage Storage Service (Queues, Blobs, Files) label Oct 23, 2025
@wilgert
Copy link

wilgert commented Oct 28, 2025

It would be great if this could be merged and released soon!
We ran into an issue with using a BlobTrigger on a different storage account than the AzureWebJobsStorage.
When processing the message failed it tries to put the message on the poison queue in the storage account that contains the Blob instead of the AzureWebJobsStorage.
Since we authorise with that other storage account using Managed Identity with minimal role assignment (only Blob Data Reader) it causes an unhandled exception that triggers a container restart.

We have also opened a case with Azure Support about this (#2510070050000897).

Trace that show restart

An unhandled exception has occurred. Host is shutting down.

Exceptiont that is logged just before this trace

This request is not authorized to perform this operation using this permission.
RequestId:9b46758f-4003-004e-3179-33e8f1000000
Time:2025-10-02T08:52:12.6087860Z
Status: 403 (This request is not authorized to perform this operation using this permission.)
ErrorCode: AuthorizationPermissionMismatch

Content:
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthorizationPermissionMismatch</Code><Message>This request is not authorized to perform this operation using this permission.
RequestId:9b46758f-4003-004e-3179-33e8f1000000
Time:2025-10-02T08:52:12.6087860Z</Message></Error>

Headers:
Server: Windows-Azure-Queue/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: 9b46758f-4003-004e-3179-33e8f1000000
x-ms-client-request-id: 8dcac883-be71-40c2-a7ec-c63cc4f2681b
x-ms-version: 2025-01-05
x-ms-error-code: AuthorizationPermissionMismatch
Date: Thu, 02 Oct 2025 08:52:12 GMT
Content-Length: 279
Content-Type: application/xml

Stacktrace for this exception

[
  {
    "assembly": "Azure.Storage.Queues, Version=12.21.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Azure.Storage.Queues.MessagesRestClient+&lt;EnqueueAsync&gt;d__14.MoveNext",
    "level": 0,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 1,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 2,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 3,
    "line": 0
  },
  {
    "assembly": "Azure.Storage.Queues, Version=12.21.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Azure.Storage.Queues.QueueClient+&lt;SendMessageInternal&gt;d__82.MoveNext",
    "level": 4,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 5,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 6,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 7,
    "line": 0
  },
  {
    "assembly": "Azure.Storage.Queues, Version=12.21.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Azure.Storage.Queues.QueueClient+&lt;SendMessageAsync&gt;d__81.MoveNext",
    "level": 8,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 9,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 10,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 11,
    "line": 0
  },
  {
    "assembly": "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.4.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Microsoft.Azure.WebJobs.Extensions.Storage.Common.StorageQueueExtensions+&lt;AddMessageAndCreateIfNotExistsAsync&gt;d__0.MoveNext",
    "level": 12,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 13,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 14,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 15,
    "line": 0
  },
  {
    "assembly": "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.4.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Microsoft.Azure.WebJobs.Host.Queues.QueueProcessor+&lt;CopyMessageToPoisonQueueAsync&gt;d__14.MoveNext",
    "level": 16,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 17,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 18,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 19,
    "line": 0
  },
  {
    "assembly": "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.4.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Microsoft.Azure.WebJobs.Host.Queues.QueueProcessor+&lt;HandlePoisonMessageAsync&gt;d__13.MoveNext",
    "level": 20,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 21,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 22,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 23,
    "line": 0
  },
  {
    "assembly": "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.4.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Microsoft.Azure.WebJobs.Host.Queues.QueueProcessor+&lt;BeginProcessingMessageAsync&gt;d__11.MoveNext",
    "level": 24,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw",
    "level": 25,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess",
    "level": 26,
    "line": 0
  },
  {
    "assembly": "System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "method": "System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification",
    "level": 27,
    "line": 0
  },
  {
    "assembly": "Microsoft.Azure.WebJobs.Extensions.Storage.Blobs, Version=5.3.4.0, Culture=neutral, PublicKeyToken=92742159e12e44c8",
    "method": "Microsoft.Azure.WebJobs.Extensions.Storage.Common.Listeners.QueueListener+&lt;ProcessMessageAsync&gt;d__44.MoveNext",
    "level": 28,
    "line": 0
  }
]

See support case #2510070050000897.

Copy link
Member

@mathewc mathewc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial feedback

@@ -11,7 +11,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.Storage.Blobs.Listeners
{
internal interface IBlobNotificationStrategy : ITaskSeriesCommand, IBlobWrittenWatcher
{
Task RegisterAsync(BlobServiceClient blobServiceClient, BlobContainerClient container, ITriggerExecutor<BlobTriggerExecutorContext> triggerExecutor,
Task RegisterAsync(
BlobServiceClient primaryBlobServiceClient,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be passing this "primary" account down anymore. It's not needed. There are only 3 derived strategies and:

  • PollLogsStrategy: Should be pointing to the $logs of the TARGET account, not host account, which is the bug you're fixing. Doesn't need host account.
  • ScanBlobScanLogHybridPollingStrategy: creates a PollLogsStrategy internally, so the above applies. Additionally, it's using a BlobScanInfoManager instance which itself is pointing at the host storage account already which is correct - we want to store these scan breadcrumbs in host storage not target storage. However the AccountName being passed into IBlobScanInfoManager.LoadLatestScanAsync is currently the host storage account name which isn't correct I think. It should be the target storage account. @brettsam please confirm.
  • ScanContainersStrategy - doesn't even use the BlobServiceClient param

So all of these strategies only need the target storage client and container.

{
BlobLogListener logListener = await BlobLogListener.CreateAsync(blobServiceClient, _logger, cancellationToken).ConfigureAwait(false);
_logListeners.Add(blobServiceClient, logListener);
// If no target client is specified, use the primary.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why we need this special handling for if a target isn't specified. We're already taking a BlobContainerClient that is coming from the "target" account. Ultimately that's coming from when the trigger binding was created here. The "data" client is the "target" client. I think we can simplify here, particularly based on my other comment not to even pass the "primary" account down.

{
Logger.LoggingNotEnabledOnTargetAccount(_logger, targetBlobServiceClient.Uri.AbsoluteUri);

// Fallback to primary client if target client does not have the permissions to be used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that you're trying to be defensive here with this fallback, but we're falling back to incorrect behavior that doesn't address the bug.

I'm thinking instead, the EnableLoggingAsync should be a TryEnableLoggingAsync method that handles the permissions error and logs an error that the customer will see. The error should indicate that logging needs to be enabled on the account, we tried to do that automatically but couldn't, etc. Resolution will be for customers to do this themselves.

Ideally we'd allow this to be fatal just as it is now targeting the primary storage account, but there is a high chance for regressions here since customers may be targeting secondary accounts without the right permissions. So I think the safest course of action will be to log the message.

Note that this change will be no worse than current behavior. I.e. for customers targeting a secondary account where they don't have permissions for us to enable logging, we're already not triggering on their logs.

@@ -256,5 +278,22 @@ private void ThrowIfDisposed()
throw new ObjectDisposedException(null);
}
}

private async Task<bool> CheckLoggingEnabledAsync(BlobServiceClient blobClient, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this being used anywhere?

@mathewc
Copy link
Member

mathewc commented Oct 29, 2025

@wilgert Yes, the current behavior for poison blob handling of the special "webjobs-blobtrigger" control queue is for the corresponding poison queue "webjobs-blobtrigger-poison" to be created in the "target" storage account, only falling back to the "host" storage account (AzureWebJobsStorage) if queues aren't supported (code here).

I suspect that a bug has been introduced here during refactorings over time. The intent was that if queues aren't supported on that storage account to fall back. Currently the code will just create a queue client for the target account and that won't fail until later when it's used. I suspect that long ago we did a runtime check here for queues and did the fallback if we couldn't connect.

@wilgert
Copy link

wilgert commented Oct 30, 2025

@wilgert Yes, the current behavior for poison blob handling of the special "webjobs-blobtrigger" control queue is for the corresponding poison queue "webjobs-blobtrigger-poison" to be created in the "target" storage account, only falling back to the "host" storage account (AzureWebJobsStorage) if queues aren't supported (code here).

I suspect that a bug has been introduced here during refactorings over time. The intent was that if queues aren't supported on that storage account to fall back. Currently the code will just create a queue client for the target account and that won't fail until later when it's used. I suspect that long ago we did a runtime check here for queues and did the fallback if we couldn't connect.

Good to learn it is a known issue. So my assumption is right that this PR would fix our issue as well? Or is another change needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Storage Storage Service (Queues, Blobs, Files)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants